/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.server.async;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RpcTokenException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.AbstractRemoteServiceServlet;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;
import com.google.gwt.user.server.rpc.UnexpectedException;

@SuppressWarnings("serial")
public abstract class AsyncRemoteServiceServlet extends AbstractRemoteServiceServlet implements
		SerializationPolicyProvider
{
	/**
	 * Used by HybridServiceServlet.
	 */
	static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, HttpServletRequest request,
			String moduleBaseURL, String strongName)
	{
		/*
		 * The request can tell you the path of the web app relative to the
		 * container root.
		 */
		String contextPath = request.getContextPath();

		String modulePath = null;
		if (moduleBaseURL != null) try
		{
			modulePath = new URL(moduleBaseURL).getPath();
		}
		catch (MalformedURLException ex)
		{
			/* log the information, we will default */
			servlet.log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
		}

		SerializationPolicy serializationPolicy = null;

		/*
		 * Check that the module path must be in the same web app as the servlet
		 * itself. If you need to implement a scheme different than this,
		 * override this method.
		 */
		if (modulePath == null || !modulePath.startsWith(contextPath))
		{
			String message = "ERROR: The module path requested, " + modulePath
					+ ", is not in the same web application as this servlet, " + contextPath
					+ ".  Your module may not be properly configured or your client and server code maybe out of date.";
			servlet.log(message);
		}
		else
		{
			/*
			 * Strip off the context path from the module base URL. It should be
			 * a strict prefix.
			 */
			String contextRelativePath = modulePath.substring(contextPath.length());

			String serializationPolicyFilePath = SerializationPolicyLoader
					.getSerializationPolicyFileName(contextRelativePath + strongName);

			/* Open the RPC resource file and read its contents. */
			InputStream is = servlet.getServletContext().getResourceAsStream(serializationPolicyFilePath);
			try
			{
				if (is != null)
					try
					{
						serializationPolicy = SerializationPolicyLoader.loadFromStream(is, null);
					}
					catch (ParseException e)
					{
						servlet.log("ERROR: Failed to parse the policy file '" + serializationPolicyFilePath + "'", e);
					}
					catch (IOException e)
					{
						servlet.log("ERROR: Could not read the policy file '" + serializationPolicyFilePath + "'", e);
					}
				else
				{
					String message = "ERROR: The serialization policy file '" + serializationPolicyFilePath
							+ "' was not found; did you forget to include it in this deployment?";
					servlet.log(message);
				}
			}
			finally
			{
				if (is != null) try
				{
					is.close();
				}
				catch (IOException e)
				{
					// Ignore this error
				}
			}
		}

		return serializationPolicy;
	}

	/**
	 * A cache of moduleBaseURL and serialization policy strong name to
	 * {@link SerializationPolicy}.
	 */
	private final Map<String, SerializationPolicy> serializationPolicyCache = new HashMap<String, SerializationPolicy>();

	/**
	 * The implementation of the service.
	 */
	private final Object delegate;

	/**
	 * The default constructor used by service implementations that extend this
	 * class. The servlet will delegate AJAX requests to the appropriate method
	 * in the subclass.
	 */
	public AsyncRemoteServiceServlet()
	{
		this.delegate = this;
	}

	/**
	 * The wrapping constructor used by service implementations that are
	 * separate from this class. The servlet will delegate AJAX requests to the
	 * appropriate method in the given object.
	 */
	public AsyncRemoteServiceServlet(Object delegate)
	{
		this.delegate = delegate;
	}

	@Override
	public final SerializationPolicy getSerializationPolicy(String moduleBaseURL, String strongName)
	{
		SerializationPolicy serializationPolicy = getCachedSerializationPolicy(moduleBaseURL, strongName);
		if (serializationPolicy != null) return serializationPolicy;

		serializationPolicy = doGetSerializationPolicy(getThreadLocalRequest(), moduleBaseURL, strongName);

		if (serializationPolicy == null)
		{
			// Failed to get the requested serialization policy; use the default
			log("WARNING: Failed to get the SerializationPolicy '"
					+ strongName
					+ "' for module '"
					+ moduleBaseURL
					+ "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
			serializationPolicy = RPC.getDefaultSerializationPolicy();
		}

		/*
		 * This could cache null or an actual instance. Either way we will not
		 * attempt to lookup the policy again.
		 */
		putCachedSerializationPolicy(moduleBaseURL, strongName, serializationPolicy);

		return serializationPolicy;
	}

	/**
	 * Process a call originating from the given request. The process check if
	 * the requested method has an async version (the same as in the interface
	 * extending {@link RemoteService}) but with last argument of type
	 * {@link AsyncManager}.
	 * 
	 * @param payload the UTF-8 request payload
	 * @param response
	 * @param request
	 * @throws SerializationException if we cannot serialize the response
	 * @throws IOException
	 * @throws UnexpectedException if the invocation throws a checked exception
	 *         that is not declared in the service method's signature
	 * @throws RuntimeException if the service method throws an unchecked
	 *         exception (the exception will be the one thrown by the service)
	 */
	public void processCall(String payload, final HttpServletRequest request, final HttpServletResponse response)
			throws SerializationException, IOException
	{
		// First, check for possible XSRF situation
		checkPermutationStrongName();

		String respPayload;
		try
		{
			final RPCRequest rpcRequest = RPC.decodeRequest(payload, this.delegate.getClass(), this);
			onAfterRequestDeserialized(rpcRequest);

			/*
			 * Checking if there exists a local async method, in such case it
			 * will be prepared
			 */
			final Method method = rpcRequest.getMethod();
			String methodName = rpcRequest.getMethod().getName();

			Class<?>[] paramTypes = rpcRequest.getMethod().getParameterTypes();
			Class<?>[] paramTypesWithAsync = Arrays.copyOf(paramTypes, paramTypes.length + 1);
			paramTypesWithAsync[paramTypes.length] = AsyncManager.class;

			Object[] parameters = rpcRequest.getParameters();

			try
			{
				Method asyncMethod = this.delegate.getClass().getMethod(methodName, paramTypesWithAsync);

				AsyncManager<?> asyncManager = new AsyncManager<Object>(request, response)
				{
					@Override
					public void doCompleteForSuccess(Object returnValue) throws SerializationException, IOException
					{
						String asyncPayload = RPC.encodeResponseForSuccess(method, returnValue,
								rpcRequest.getSerializationPolicy(), rpcRequest.getFlags());
						writeResponse(request, response, asyncPayload);

						doComplete();
					}

					@Override
					public void doCompleteForFailure(Throwable exception) throws SerializationException, IOException
					{
						String asyncErrorPayload = RPC.encodeResponseForFailure(method, exception,
								rpcRequest.getSerializationPolicy(), rpcRequest.getFlags());
						writeResponse(request, response, asyncErrorPayload);

						doComplete();
					}
				};

				Object[] asyncParams = Arrays.copyOf(parameters, parameters.length + 1);
				asyncParams[parameters.length] = asyncManager;

				try
				{
					asyncMethod.invoke(this.delegate, asyncParams);
				}
				catch (Exception e)
				{
					/*
					 * In case the invocation produces an error the exception
					 * will be transformed into a response.
					 */
					String asyncErrorPayload = RPC.encodeResponseForFailure(method, e,
							rpcRequest.getSerializationPolicy(), rpcRequest.getFlags());
					writeResponse(request, response, asyncErrorPayload);
				}
			}
			catch (NoSuchMethodException e)
			{
				/* In such case there isn't an async version of the method. */
				respPayload = RPC.invokeAndEncodeResponse(this.delegate, method, parameters,
						rpcRequest.getSerializationPolicy(), rpcRequest.getFlags());

				/* Let subclasses see the serialized response. */
				onAfterResponseSerialized(respPayload);
				/* Write the response. */
				writeResponse(request, response, respPayload);
			}
		}
		catch (IncompatibleRemoteServiceException ex)
		{
			log("An IncompatibleRemoteServiceException was thrown while processing this call.", ex);
			respPayload = RPC.encodeResponseForFailure(null, ex);
		}
		catch (RpcTokenException tokenException)
		{
			log("An RpcTokenException was thrown while processing this call.", tokenException);
			respPayload = RPC.encodeResponseForFailure(null, tokenException);
		}
	}

	/**
	 * Standard HttpServlet method: handle the POST. This doPost method swallows
	 * ALL exceptions, logs them in the ServletContext, and returns a
	 * GENERIC_FAILURE_MSG response with status code 500.
	 * 
	 * @throws ServletException
	 * @throws SerializationException
	 */
	@Override
	public final void processPost(HttpServletRequest request, HttpServletResponse response) throws IOException,
			ServletException, SerializationException
	{
		/* Read the request fully. */
		String requestPayload = readContent(request);

		/* Let subclasses see the serialized request. */
		onBeforeRequestDeserialized(requestPayload);

		/* Invoke the core dispatching logic */
		processCall(requestPayload, request, response);
	}

	/**
	 * This method is called by {@link #processCall(String)} and will throw a
	 * SecurityException if {@link #getPermutationStrongName()} returns
	 * <code>null</code>. This method can be overridden to be a no-op if there
	 * are clients that are not expected to provide the
	 * {@value com.google.gwt.user.client.rpc.RpcRequestBuilder#STRONG_NAME_HEADER}
	 * header.
	 * 
	 * @throws SecurityException if
	 *         {@link AbstractRemoteServiceServlet#getPermutationStrongName()}
	 *         returns <code>null</code>
	 */
	protected void checkPermutationStrongName() throws SecurityException
	{
		if (getPermutationStrongName() == null)
			throw new SecurityException("Blocked request without GWT permutation header (XSRF attack?)");
	}

	/**
	 * Gets the {@link SerializationPolicy} for given module base URL and strong
	 * name if there is one. Override this method to provide a
	 * {@link SerializationPolicy} using an alternative approach.
	 * 
	 * @param request the HTTP request being serviced
	 * @param moduleBaseURL as specified in the incoming payload
	 * @param strongName a strong name that uniquely identifies a serialization
	 *        policy file
	 * @return a {@link SerializationPolicy} for the given module base URL and
	 *         strong name, or <code>null</code> if there is none
	 */
	protected SerializationPolicy doGetSerializationPolicy(HttpServletRequest request, String moduleBaseURL,
			String strongName)
	{
		return loadSerializationPolicy(this, request, moduleBaseURL, strongName);
	}

	/**
	 * Override this method to examine the serialized response that will be
	 * returned to the client. The default implementation does nothing and need
	 * not be called by subclasses.
	 * 
	 * @param serializedResponse
	 */
	protected void onAfterResponseSerialized(String serializedResponse)
	{}

	/**
	 * Override this method to examine the serialized version of the request
	 * payload before it is deserialized into objects. The default
	 * implementation does nothing and need not be called by subclasses.
	 * 
	 * @param serializedRequest
	 */
	protected void onBeforeRequestDeserialized(String serializedRequest)
	{}

	/**
	 * Determines whether the response to a given servlet request should or
	 * should not be GZIP compressed. This method is only called in cases where
	 * the requester accepts GZIP encoding.
	 * <p>
	 * This implementation currently returns <code>true</code> if the response
	 * string's estimated byte length is longer than 256 bytes. Subclasses can
	 * override this logic.
	 * </p>
	 * 
	 * @param request the request being served
	 * @param response the response that will be written into
	 * @param responsePayload the payload that is about to be sent to the client
	 * @return <code>true</code> if responsePayload should be GZIP compressed,
	 *         otherwise <code>false</code>.
	 */
	protected boolean shouldCompressResponse(HttpServletRequest request, HttpServletResponse response,
			String responsePayload)
	{
		return RPCServletUtils.exceedsUncompressedContentLengthLimit(responsePayload);
	}

	private SerializationPolicy getCachedSerializationPolicy(String moduleBaseURL, String strongName)
	{
		synchronized (this.serializationPolicyCache)
		{
			return this.serializationPolicyCache.get(moduleBaseURL + strongName);
		}
	}

	private void putCachedSerializationPolicy(String moduleBaseURL, String strongName,
			SerializationPolicy serializationPolicy)
	{
		synchronized (this.serializationPolicyCache)
		{
			this.serializationPolicyCache.put(moduleBaseURL + strongName, serializationPolicy);
		}
	}

	private void writeResponse(HttpServletRequest request, HttpServletResponse response, String responsePayload)
			throws IOException
	{
		boolean gzipEncode = RPCServletUtils.acceptsGzipEncoding(request)
				&& shouldCompressResponse(request, response, responsePayload);

		RPCServletUtils.writeResponse(getServletContext(), response, responsePayload, gzipEncode);
	}
}
